package myJs.cps

import io.scalaland.chimney.TransformerFSupport
import myJs.cps.{PureSelectCp, PureTextareaCp, Style}
import myJs.myPkg.MyFetch
import slinky.core.{FunctionalComponent, SyntheticEvent, TagElement}
import slinky.core.annotations.react
import slinky.core.facade.Hooks.{useEffect, useRef, useState}
import slinky.web.html._

import scala.scalajs.js.Dynamic.{global => g}
import scala.scalajs.js.JSConverters._
import myJs.Implicits._
import myJs.cps.myTable._
import myJs.myPkg.i18n.ReactI18next
import myJs.myPkg.jquery.$
import myJs.tool.Pojo._
import myJs.tool.{Pojo, Tool}
import org.scalajs.dom.{Element, Event, document}
import slinky.core.facade.ReactElement

import scala.collection.immutable.SeqMap
import scala.concurrent.Future
import scala.scalajs.js
import org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits._
import upickle.default._
import typings.csstype.csstypeStrings
import io.scalaland.chimney.dsl._
import typings.csstype.mod.Property.JustifyContent

import scala.collection.compat.Factory

/** Created by yz on 18/3/2022
  */
@react object BootstrapTableCp {

  case class Column(
      field: String,
      title: String,
      operable: Boolean = false,
      formatter: js.UndefOr[(ujson.Value, ujson.Obj) => ReactElement] = js.undefined,
      searchKind: SearchKind = TextSearch,
      comVF: (ujson.Value, ujson.Obj) => String,
      optionMap: SeqMap[String, String] = SeqMap.empty,
      defaultValue: String = ""
  )

  case class Props(
      columns: List[Column],
      datas: List[ujson.Obj],
      pagination: Boolean = true,
      pageList: List[Int] = List(10, 25, 50, 100),
      pageSize: Int = 10,
      sidePagination: String = "client",
      ajax: js.UndefOr[(Int, Int, SeqMap[String, SearchData], Option[SortData], String) => Future[ujson.Obj]] =
        js.undefined,
      selectColumnEnabled: Boolean = false,
      refresh: Boolean = false,
      refreshChange: js.UndefOr[Boolean => Unit] = js.undefined,
      exportData: List[ExportData] = List.empty,
      allSearchTitles: List[String] = List.empty,
      showExport: Boolean = false,
      search: Boolean = false,
      showColumnNum: Int = 8,
      showColumnNames: List[String] = List.empty,
      operable: Boolean = true,
      thStyle: Style = Style(),
      tdStyle: Style = Style(),
      trStyle: Int => Style = (x) => Style()
  )

  val component = FunctionalComponent[Props] { props =>
    val (sortField, setSortField) = useState("")
    val (order, setOrder) = useState("")
    val (searchMap, setSearchMap) = useState(SeqMap[String, SearchData]())
    val (tmpSearchMap, setTmpSearchMap) = useState(SeqMap[String, SearchData]())
    val (pageNumber, setPageNumber) = useState(1)
    val (pageSize, setPageSize) = useState(props.pageSize)
    val (menuField, setMenuField) = useState("")
    val (menuShow, setMenuShow) = useState(false)
    val (serverDatas, setServerDatas) = useState(List.empty[ujson.Obj])
    val (serverTotalRows, setServerTotalRows) = useState(0)
    val (columnShowMap, setColumnShowMap) = useState(SeqMap[String, Boolean]())
    val (hasFillDefaultV, setHasFillDefaultV) = useState(false)
    val (allSearch, setAllSearch) = useState("")

    val js.Tuple3(t, i18n, _) = ReactI18next.useTranslation()
    val isServer = props.sidePagination == "server"

    val fieldColumnMap = props.columns.map { x =>
      (x.field, x)
    }.toSeqMap

    object FTool {

      def allSearchChange = (v: String) => {
        setAllSearch(v)
      }

      def sortExec = {
        val initDatas = props.datas
        sortField.isBlank
        val (ascSortData, blankDatas) = if (sortField.isBlank) {
          (initDatas, List())
        } else {
          val column = fieldColumnMap(sortField)
          column.searchKind match {
            case Pojo.NumericSearch =>
              val noDoubleDatas = initDatas.filterNot(_(sortField).str.isDouble)
              val doubleDatas = initDatas.filter(_(sortField).str.isDouble).sortBy { x =>
                column.comVF(x(sortField), x.obj).toDouble
              }
              (doubleDatas, noDoubleDatas)
            case _ =>
              val sortData = initDatas.sortBy(_.obj(sortField).str)
              (sortData, List())
          }
        }
        order match {
          case "desc" => ascSortData.reverse ::: blankDatas
          case "asc" | "" => ascSortData ::: blankDatas
        }
      }

      def execFilter(sortDatas: List[ujson.Obj]) = {

        def loop(acc: List[ujson.Obj], searchs: List[(String, SearchData)]): List[ujson.Obj] = {
          searchs match {
            case Nil => acc
            case y :: ys =>
              val curFilterData = acc.filter { row =>
                val field = y._1
                val searchData = y._2
                val column = fieldColumnMap(field)
                searchData match {
                  case DateSearchData(startDate, endDate) =>
                    val dbDate = row(field)
                    val startB = if (startDate.nonBlank) {
                      Tool.timeBefore(startDate, dbDate.toString)
                    } else true
                    val endB = if (endDate.nonBlank) {
                      Tool.timeAfter(endDate, dbDate.toString)
                    } else true
                    startB && endB
                  case TextSearchData(searchType, text) =>
                    val vs = text.split(";")
                    searchType match {
                      case "equals" =>
                        vs.exists(v => row(field).str == v)
                      case "like" =>
                        vs.exists(v => row(field).str.contains(v))
                    }
                  case NumericSearchData(min, max, kind) =>
                    val v = column.comVF(row(field), row)
                    v.isDouble && {
                      val dbDouble = v.toDouble
                      val minB = if (min.isDouble) {
                        dbDouble >= min.toDouble
                      } else true
                      val maxB = if (max.isDouble) {
                        dbDouble <= max.toDouble
                      } else true
                      minB && maxB
                    }
                  case CheckboxSearchData(values) =>
                    values.exists(x => row(field).str.split(";").contains(x))
                }
              }
              loop(curFilterData, ys)
          }
        }

        loop(sortDatas, searchMap.toList)
      }

      def allSearchFilter(datas: List[ujson.Obj], titles: List[String]) = {
        val titleFieldMap = fieldColumnMap.map { case (field, column) =>
          (column.title, field)
        }.toMap
        if (titles.isEmpty) {
          datas
        } else {
          datas.filter { row =>
            titles.exists { title =>
              val field = titleFieldMap(title)
              row(field).str.contains(allSearch)
            }
          }
        }
      }

      def removeSort = () => {
        setSortField("")
        setOrder("")
      }

      def tmp2True(tmpSearchMap: SeqMap[String, SearchData]) = {
        tmpSearchMap.filter { case (field, searchData) =>
          searchData match {
            case DateSearchData(startDate, endDate) => startDate.nonBlank || endDate.nonBlank
            case TextSearchData(searchType, text) => text.nonBlank
            case NumericSearchData(min, max, kind) => min.nonBlank || max.nonBlank
            case CheckboxSearchData(values) => values.nonEmpty
          }
        }
      }

      val removeCondition = (curField: String) => {
        val curTmpSearchMap = tmpSearchMap.updatedWith(curField) { op =>
          op.map { x =>
            x match {
              case y @ DateSearchData(startDate, endDate) => y.copy(startDate = "", endDate = "")
              case y @ TextSearchData(searchType, text) => y.copy(text = "")
              case y @ NumericSearchData(min, max, kind) => y.copy(min = "", max = "")
              case y @ CheckboxSearchData(values) => y.copy(values = List.empty[String])
            }
          }
        }
        setTmpSearchMap(curTmpSearchMap)
        val curSearchMap = FTool.tmp2True(curTmpSearchMap)
        setSearchMap(curSearchMap)
      }

      def confirmFilter(curField: String) = () => {
        val curSearchMap = FTool.tmp2True(tmpSearchMap)
        setSearchMap(curSearchMap)
        setMenuShow(false)
      }

      val pageSizeChange = (x: Int) => {
        setPageSize(x)
      }

      val pageNumberChange = (x: Int) => {
        setPageNumber(x)
      }

      def refreshServerDatas = {
        if (isServer) {
          props.ajax.map { ajax =>
            val offset = pageSize * (pageNumber - 1)
            val limit = pageSize
            val sortData = if (sortField.nonBlank) {
              val column = fieldColumnMap(sortField)
              val kind = column.searchKind match {
                case Pojo.NumericSearch => "int"
                case Pojo.DoubleSearch => "double"
                case _ => "str"
              }
              Some(SortData(field = sortField, kind = kind, order = order))
            } else None
            ajax(limit, offset, searchMap, sortData, allSearch).map { obj =>
              val tableRs = obj("rows").transform(reader[List[ujson.Obj]])
              val total = obj("total").num.toInt
              setServerDatas(tableRs)
              setServerTotalRows(total)
            }
          }
        }
      }

      val columnShowMapChange = (x: SeqMap[String, Boolean]) => {
        setColumnShowMap(x)
      }

    }

    val datas = if (isServer) {
      serverDatas
    } else {
      val sortDatas = FTool.sortExec
      val filterData = FTool.execFilter(sortDatas)
      FTool.allSearchFilter(filterData, props.allSearchTitles)
    }

    val totalRows = if (isServer) {
      serverTotalRows
    } else datas.value.size
    val pageTo = math.min(pageNumber * pageSize, totalRows)

    val pageFrom = (pageNumber - 1) * pageSize + 1

    val curPageDatas = if (isServer) {
      datas
    } else datas.slice(pageFrom - 1, pageTo)

    useEffect(
      () => {
        $(document).on(
          "click",
          (y: Element, e: Event) => {
            val trueY = e.target.asInstanceOf[Element]
            val b = $(trueY).hasClass("select2-selection__choice__remove") ||
              $(trueY).hasClass("select2-search__field") ||
              $(trueY).hasClass("filterContent") ||
              $(trueY).parents().hasClass("filterContent")
            if (!b) {
              setMenuShow(false)
            }
          }
        )
      },
      List()
    )

    useEffect(
      () => {
        FTool.refreshServerDatas
      },
      List(pageSize, pageNumber, sortField, order, searchMap)
    )

    useEffect(
      () => {
        val delayFn = js.timers.setTimeout(500) {
          FTool.refreshServerDatas
        }
        () => {
          js.timers.clearTimeout(delayFn)
        }
      },
      List(allSearch)
    )

    useEffect(
      () => {
        if (props.refresh) {
          FTool.refreshServerDatas
          props.refreshChange.foreach(_(false))
        }
      },
      List(props.refresh)
    )

    useEffect(
      () => {
        if (hasFillDefaultV) {
          props.columns.headOption.map { column =>
            FTool.confirmFilter(column.field)()
          }
          setHasFillDefaultV(false)
        }
      },
      List(hasFillDefaultV)
    )

    useEffect(
      () => {
        val initTmpSearchMap = props.columns.map { x =>
          val searchData = (x.searchKind match {
            case DateSearch => DateSearchData(startDate = "", endDate = "")
            case TextSearch => TextSearchData(searchType = "like", text = "")
            case NumericSearch => NumericSearchData(min = "", max = "", kind = "int")
            case DoubleSearch => NumericSearchData(min = "", max = "", kind = "double")
            case CheckboxSearch =>
              val curVs = if (x.defaultValue.isBlank) List.empty[String] else List(x.defaultValue)
              CheckboxSearchData(values = curVs)
          }).asInstanceOf[SearchData]
          (x.field, searchData)
        }.toSeqMap
        setTmpSearchMap(initTmpSearchMap)
        setHasFillDefaultV(true)

        val initColumnMap = if (props.selectColumnEnabled) {
          if (props.showColumnNames.nonEmpty) {
            props.columns.map { column =>
              (column.field, props.showColumnNames.contains(column.title))
            }.toSeqMap
          } else {
            props.columns.zipWithIndex.map { case (column, curI) =>
              (column.field, curI < props.showColumnNum)
            }.toSeqMap
          }

        } else {
          props.columns.zipWithIndex.map { case (column, curI) =>
            (column.field, true)
          }.toSeqMap
        }
        setColumnShowMap(initColumnMap)
      },
      List(props.columns)
    )

    val minHeight = if (props.pagination && props.operable) 350 else 0

    div(
      if (props.selectColumnEnabled) {
        SelectColumnCp(
          columnShowMap = columnShowMap,
          fieldColumnMap = fieldColumnMap,
          columnShowMapChange = FTool.columnShowMapChange
        )
      } else null,
      FilterConditionCp(
        searchMap = searchMap,
        fieldColumnMap = fieldColumnMap,
        removeCondition = FTool.removeCondition
      ),
      SortConditionCp(
        fieldColumnMap = fieldColumnMap,
        sortField = sortField,
        order = order,
        removeSort = FTool.removeSort
      ),
      div(
        className := "table-responsive",
        div(
          className := "bootstrap-table bootstrap3",
          div(
            className := "fixed-table-toolbar",
            if (props.showExport) {
              div(
                className := "columns columns-right btn-group pull-right",
                div(
                  className := "export btn-group",
                  button(
                    className := "btn btn-default dropdown-toggle",
                    aria - "label" := "Export",
                    data - "toggle" := "dropdown",
                    title := "Export data",
                    i(className := "glyphicon glyphicon-export icon-share"),
                    " ",
                    span(className := "caret")
                  ),
                  ul(
                    className := "dropdown-menu",
                    role := "menu",
                    props.exportData.map { x =>
                      li(
                        key := x.text,
                        role := "menuitem",
                        x.href.toOption match {
                          case Some(hrefStr) => a(href := hrefStr, x.text)
                          case None => a(onClick := x.onClick.get, x.text)
                        }
                      )
                    }
                  )
                )
              )
            } else {
              null
            },
            if (props.search) {
              div(
                className := "pull-right search input-group",
                PureInputCp(
                  selfClassName = "fsearch-input",
                  placeHolder = "Search",
                  autoComplete = "off",
                  value = allSearch,
                  onChange = FTool.allSearchChange
                )
              )
            } else null
          ),
          div(
            style := Style(paddingBottom = 0, overflowY = csstypeStrings.auto, minHeight = minHeight),
            className := "fixed-table-container",
            div(
              className := "fixed-table-body",
              style := Style(overflow = "visible"),
              table(
                style := Style(wordWrap = csstypeStrings.`break-word`),
                id := "myTable",
                className := "display table table-hover table-bordered",
                thead(
                  tr(
                    props.columns.zipWithIndex
                      .filter { case (column, curI) =>
                        columnShowMap.getOrElse(column.field, true)
                      }
                      .map { case (column, curI) =>
                        val initThStyle = Style(
                          overflow = "visible",
                          display = csstypeStrings.flex,
                          justifyContent = csstypeStrings.`space-between`
                        )
                        val thStyle = if (props.thStyle.justifyContent.isDefined) {
                          initThStyle.copy(justifyContent = props.thStyle.justifyContent)
                        } else initThStyle
                        th(
                          key := s"${column.field}",
                          div(
                            className := "th-inner",
                            style := thStyle,
                            s"${column.title} ",
                            FilterButtonCp(
                              column = column,
                              sortField = sortField,
                              sortFieldChange = setSortField,
                              order = order,
                              orderChange = setOrder,
                              menuShow = menuShow,
                              menuShowChange = setMenuShow,
                              menuField = menuField,
                              menuFieldChange = setMenuField,
                              tmpSearchMap = tmpSearchMap,
                              confirmFilter = FTool.confirmFilter,
                              tmpSearchMapChange = setTmpSearchMap
                            )
                          ),
                          div(className := "fht-cell")
                        )
                      }
                  )
                ),
                tbody(
                  if (datas.value.isEmpty) {
                    tr(className := "no-records-found", td(colSpan := props.columns.size, t("noMatches")))
                  } else {
                    curPageDatas.zipWithIndex.map { case (curData, curI) =>
                      val trStyle = props.trStyle(curI)
                      tr(
                        style := trStyle,
                        key := s"${curI}",
                        data - "index" := s"${curI}",
                        props.columns
                          .filter { column =>
                            columnShowMap.getOrElse(column.field, true)
                          }
                          .map { column =>
                            val any = curData(column.field)
                            val ele = column.formatter
                              .map { f =>
                                f(any, curData)
                              }
                              .getOrElse {
                                span(curData(column.field).str)()
                              }
                            td(style := props.tdStyle, key := s"${column.field}", ele)
                          }
                      )
                    }
                  }
                )
              )
            )
          ),
          if (props.pagination) {
            PaginationCp(
              totalRows = totalRows,
              pageFrom = pageFrom,
              pageTo = pageTo,
              pageList = props.pageList,
              pageSize = pageSize,
              pageSizeChange = FTool.pageSizeChange,
              pageNumber = pageNumber,
              pageNumberChange = FTool.pageNumberChange
            )
          } else null
        )
      )
    )

  }

}
